package com.hsurosy.domain.strategy.service.armory;

import com.hsurosy.domain.strategy.model.entity.StrategyAwardEntity;
import com.hsurosy.domain.strategy.model.entity.StrategyEntity;
import com.hsurosy.domain.strategy.model.entity.StrategyRuleEntity;
import com.hsurosy.domain.strategy.repository.IStrategyRepository;
import com.hsurosy.types.common.Constants;
import com.hsurosy.types.enums.ResponseCode;
import com.hsurosy.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.SecureRandom;
import java.util.*;

/**
 * @Author Hsu琛君珩
 * @Date 2024-10-15 17:04
 * @Description 抽奖策略装配库，负责初始化策略计算
 * @Version: v1.0.0
 */
@Slf4j
@Service
public class StrategyArmoryDispatch implements IStrategyArmory, IStrategyDispatch {

    @Resource
    private IStrategyRepository repository;

    /**
     * 装配抽奖策略，根据每个奖品的概率生成概率查找表，并将其存储在 Redis 中。
     *
     * @param strategyId 抽奖策略ID，用于标识特定的抽奖策略
     * @return 返回 true 表示装配成功
     */
    @Override
    public boolean assembleLotteryStrategy(Long strategyId) {
        // 1. 查询策略ID对应的奖品列表，包含每个奖品的概率信息
        // 奖品A：ID = 1，概率 = 0.10，奖品B：ID = 2，概率 = 0.05，奖品C：ID = 3，概率 = 0.01
        List<StrategyAwardEntity> strategyAwardEntities = repository.queryStrategyAwardList(strategyId);

        // 2. 将每个奖品的库存信息缓存到 Redis 中【用于 decr 扣减库存使用】
        for (StrategyAwardEntity strategyAward : strategyAwardEntities) {
            Integer awardId = strategyAward.getAwardId();  // 奖品ID
            Integer awardCount = strategyAward.getAwardCount();  // 奖品库存
            cacheStrategyAwardCount(strategyId, awardId, awardCount);  // 将奖品库存缓存到 Redis 中
        }

        // 3.1 默认装配策略：为策略ID生成默认的概率查找表，并存储到 Redis 中
        assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities);  // 装配默认抽奖策略

        // 3.2 权重策略装配：适用于权重规则配置的抽奖策略
        StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId);  // 查询策略实体
        String ruleWeight = strategyEntity.getRuleWeight();  // 获取权重规则的标识

        // 如果没有定义权重规则，则直接返回 true 表示默认策略装配完成
        if (null == ruleWeight) {
            return true;
        }

        // 查询与权重规则相关的策略规则实体，获取具体的权重配置
        StrategyRuleEntity strategyRuleEntity = repository.queryStrategyRule(strategyId, ruleWeight);

        // 如果没有找到对应的权重规则，抛出异常提示
        if (null == strategyRuleEntity) {
            throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo());
        }

        // 获取权重值映射表，格式为： 4000: [102,103,104]，5000: [102,103,104,105,106] 等
        Map<String, List<Integer>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues();
        Set<String> keys = ruleWeightValueMap.keySet();  // 获取所有权重区间的 key，例如 4000、5000 等

        // 遍历每个权重区间的 key，装配该区间的抽奖策略
        for (String key : keys) {
            List<Integer> ruleWeightValues = ruleWeightValueMap.get(key);  // 获取当前权重区间对应的奖品ID列表

            // 复制奖品列表并移除不在当前权重区间的奖品
            ArrayList<StrategyAwardEntity> strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities);
            strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId()));

            // 为当前权重区间装配抽奖策略，并存储到 Redis 中
            assembleLotteryStrategy(String.valueOf(strategyId).concat("_").concat(key), strategyAwardEntitiesClone);
            // 存储格式为：100001_4000、100001_5000 等
        }

        // 返回 true 表示装配完成
        return true;
    }

    /**
     * 将奖品数量缓存到 Redis 中
     *
     * @param strategyId 抽奖策略ID，用于标识是哪一个抽奖策略
     * @param awardId 奖品ID，用于标识是哪一个奖品
     * @param awardCount 奖品数量，表示该奖品当前的库存数量
     */
    private void cacheStrategyAwardCount(Long strategyId, Integer awardId, Integer awardCount) {
        // 1. 生成缓存的 key，格式为 "strategy_award_count_{strategyId}_{awardId}"
        //    cacheKey 的结构为：策略ID + "_" + 奖品ID，唯一标识每个策略下的不同奖品数量
        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;

        // 2. 调用仓储层的方法，将奖品的数量（awardCount）缓存到 Redis 中，使用生成的 cacheKey 作为唯一标识
        repository.cacheStrategyAwardCount(cacheKey, awardCount);
    }

    /**
     * 根据奖品的概率生成概率查找表，并存储在Redis中
     *
     * @param key 抽奖策略的唯一标识
     * @param strategyAwardEntities 奖品列表，包含奖品ID和对应的概率
     */
    private void assembleLotteryStrategy(String key, List<StrategyAwardEntity> strategyAwardEntities) {
        // 1. 获取奖品列表中最小的奖品概率值（用于确定最小概率单位）
        // 比如 0.01
        BigDecimal minAwardRate = strategyAwardEntities.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .min(BigDecimal::compareTo)
                .orElse(BigDecimal.ZERO);
        // 2. 获取所有奖品概率的总和，作为计算基数
        // 比如 总概率 = 0.10 + 0.05 + 0.01 = 0.16
        BigDecimal totalAwardRate = strategyAwardEntities.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // 3. 计算概率范围，totalAwardRate / minAwardRate，确定概率查找表的大小
        //    这里的rateRange用于放大概率，比如百分位、千分位、万分位等
        // 比如 rateRange = 0.16 / 0.01 = 16
        BigDecimal rateRange = totalAwardRate.divide(minAwardRate, RoundingMode.CEILING); // 向上取整
        List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue());
        // 4. 对每个奖品，根据其概率值，将其ID填充到查找表中
        // 比如 A：0.10 * 16 = 1.6 （向上取整）= 2 B：0.05 * 16 = 0.8 （向上取整）= 1 C：0.01 * 16 = 0.16 （向上取整）= 1
        // 这里举个例子
        // 奖品A（ID=102，概率=10.00%）：rateRange * 10.00 = 16 * 10.00 = 160（大概会填充 160 个位置）。
        // 奖品B（ID=103，概率=4.00%）：rateRange * 4.00 = 16 * 4.00 = 64（大概填充 64 个位置）。
        // 奖品C（ID=104，概率=1.00%）：rateRange * 1.00 = 16 * 1.00 = 16（大概填充 16 个位置）。
        // 奖品D（ID=105，概率=1.00%）：rateRange * 1.00 = 16 * 1.00 = 16（大概填充 16 个位置）。
        // 所以 strategyAwardSearchRateTables 最终 size 为 256
        for (StrategyAwardEntity strategyAward : strategyAwardEntities) {
            Integer awardId = strategyAward.getAwardId(); // 奖品ID
            BigDecimal awardRate = strategyAward.getAwardRate(); // 中奖概率
            // 将奖品的ID按照其概率占用查找表中的空间
            // 这里比如 概率范围是 16，奖品中奖概率是 10，那就放 160 个这个奖品进入 strategyAwardSearchRateTables 中，向上取整是比如把 159.9 当做 160
            for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) {
                strategyAwardSearchRateTables.add(awardId);
            }
            // 比如 查找表 = [A, A, B, C]
        }
        // 5. 将查找表打乱顺序，以保证抽奖的公平性
        // 比如 查找表 = [A, C, B, A]
        Collections.shuffle(strategyAwardSearchRateTables);
        // 6. 将打乱后的查找表转换为HashMap形式，方便查找
        // 比如 查找表映射 = {
        //          0 -> A,
        //          1 -> C,
        //          2 -> B,
        //          3 -> A
        //      }
        Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>();
        for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
            shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i));
        }
        // 7. 将最终的概率查找表和其大小存储到Redis缓存中
        repository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable);
    }

    /**
     * 随机抽取奖品，基于策略ID生成一个随机值来决定抽中的奖品。
     * 使用 Redis 中存储的概率查找表来进行奖品的抽取。
     *
     * @param strategyId 抽奖策略ID
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(Long strategyId) {
        // 分布式部署下，不一定为当前应用做的策略装配，需从 Redis 中获取策略的概率范围
        int rateRange = repository.getRateRange(strategyId);
        // 通过生成随机值，依据概率查找表来确定抽中的奖品
        return repository.getStrategyAwardAssemble(String.valueOf(strategyId), new SecureRandom().nextInt(rateRange));
    }

    /**
     * 根据策略ID和权重规则值随机抽取奖品。
     * 使用 Redis 中存储的概率查找表来进行奖品的抽取，并通过权重规则来确定具体的查找范围。
     *
     * @param strategyId 抽奖策略ID
     * @param ruleWeightValue 权重规则值
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(Long strategyId, String ruleWeightValue) {
        // 生成包含策略ID和权重规则的 Redis key
        String key = String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(ruleWeightValue);
        // 使用生成的 key 来随机抽取奖品
        return getRandomAwardId(key);
    }

    /**
     * 根据给定的 Redis key 随机抽取奖品。
     * 该方法与上一个方法类似，只是通过具体的 Redis key 直接操作。
     *
     * @param key Redis 中存储策略信息的 key
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(String key) {
        // 从 Redis 中获取策略的概率范围
        int rateRange = repository.getRateRange(key);
        // 通过生成随机值，依据概率查找表来确定抽中的奖品
        return repository.getStrategyAwardAssemble(key, new SecureRandom().nextInt(rateRange));
    }

    /**
     * 扣减指定奖品的库存。
     * 从 Redis 中查找存储的奖品库存信息，并减少相应的库存数量。
     *
     * @param strategyId 抽奖策略ID
     * @param awardId 奖品ID
     * @return 如果库存扣减成功，返回 true；否则返回 false
     */
    @Override
    public Boolean subtractionAwardStock(Long strategyId, Integer awardId) {
        // 生成 Redis 缓存的 key，用于存储奖品库存
        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;
        // 从 Redis 中扣减奖品库存
        return repository.subtractionAwardStock(cacheKey);
    }

}
